Split 300-line files into focused modules (Phase 7.2)#33
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Fixes the invalid-pointer-dereference advisory in crossbeam-epoch's fmt::Pointer impl. Semver-compatible lockfile-only bump; the remaining cargo-audit findings are the three documented transitive quick-xml copies still blocked on upstream releases (see CLAUDE.md tech debt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Performance (loki-layout / loki-vello / loki-ooxml): - para_emit: build the per-glyph scale/rise vectors only when a span actually carries w:w scale or w:position baseline-shift — three heap allocations avoided per glyph run on the common path; resolve the covering span once per run instead of re-scanning spans for each of the 6 per-run attributes (link, super/sub, highlight, shadow, underline/strike variants). - para_cache: stream the Debug-format cache-key bytes straight into the hasher via a fmt::Write adapter instead of allocating a String per paragraph per layout pass (paid even on cache hits). - flow/flow_para: new para_keep_with_next() probe resolves only the keep-with-next flag for the paginated-loop pre-check and chain scan, replacing two full ParaProps clone+merge+map resolves per paragraph. - xlsx import: single-pass <c> attribute read (local_attr_vals) instead of three scans per cell; cell_ref_to_coord is now allocation-free. - FontDataCache: cache variable-font normalized coords as F2Dot14 bits (Vec<i16>) so paint_glyph_run stops rebuilding the vector per run. Dead code: - Remove unreferenced OOXML helpers (bool_attr, twips_to_points, half_points_to_points, emu_to_points, parse_twips) + their tests and the four unit constants only they used — production code uses the mapper-local conversions. - Remove the four span_*_for_range helpers superseded by the single covering-span lookup. - Delete tests/diagnose_import.rs — an assertion-free println debugging harness (per the no-committed-diagnostics rule); its fixture stays (bookmark_compat.rs uses it). Unused dependencies removed: - loki-text: dirs, serde, serde_json, vello, peniko, kurbo, wgpu_context, anyrender_vello, loki-vello (persistence + GPU surface moved to loki-app-shell/loki-renderer; comments were stale). - loki-spreadsheet / loki-presentation: same stale graphics stack plus unused model/layout deps (verified no source references, and the android_main! macro expansion only touches deps that remain). - appthere-ui: thiserror; loki-renderer: tokio; loki-server-store: serde, tracing; loki-server-api: base64. - fontique kept everywhere (documented feature-unification hack). Clippy (test targets): fix needless-borrow/collapsible-if in loki-vello's visual_conformance and needless lifetimes in two loki-text test helpers. Docs: ratchet file-ceiling baseline (para.rs 1401, xlsx/import.rs 574), sync CLAUDE.md sizes and the 2026-07-11 audit re-check note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
clippy --all-targets flags runtime assert! on compile-time-constant
token comparisons; const { assert!(..) } checks them at compile time
instead. Test-target-only lint; the library gate was already clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
- Replace default()-then-assign with struct-literal initializers across the Loro bridge/mutation/style test files (field_reassign_with_default). - Collapse nested if-lets in bookmark_tests (collapsible_if). - Move tests/helpers.rs to tests/helpers/mod.rs so cargo stops compiling the shared helper module as its own integration-test crate (which flagged every helper as dead code under --all-targets linting). - Drop a useless String into() in table_structural_ops. The library clippy gate was already clean; this extends cleanliness to test targets, which CI does not yet lint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
loki-ooxml and loki-odf opt into clippy::pedantic at the crate root, but their #[path]-included test modules were never linted (the CI gate runs without --all-targets). Fixes: concrete T::default() instead of Default::default() in struct literals, struct-literal init instead of default()-then-assign, digit separators in EMU literals, hoisted a mid-function use, named the only other enum variant instead of a wildcard arm, and clippy --fix's mechanical map_or/collapsible-if cleanups. Scoped allows with comments where the lint fights test idiom: float_cmp on exact-representable point conversions, cast_precision_loss on u8-range hex components, type_complexity on two fixture bundles, and dead_code on the shared tests/helpers module (each integration-test crate uses a different subset). cargo clippy --workspace --all-targets -- -D warnings is now fully clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The previous pass replaced Default::default() with concrete T::default() calls in test files whose glob imports did not actually bring those types into scope; import CharProps / IndexMap / NodeAttr explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…sture (7.4) Closes the four audit-2026-06 security tails (deferred-features plan 7.4): - S-1b: nested DOCX tables and content controls now carry a shared MAX_NESTING_DEPTH (100) budget through parse_table -> row -> cell and parse_sdt recursion, returning a typed OoxmlError::NestingTooDeep instead of risking stack exhaustion on crafted documents. Guarded positive (2-deep parses) and negative (150-deep rejected) tests. - S-2: emu_to_pt clamps its raw i64 input to +/-100m of EMU so an absurd cx/cy attribute cannot flow unbounded into layout arithmetic. - S-3: transcode_utf16_to_utf8 rejects an odd-length UTF-16 payload instead of silently truncating the trailing byte. - S-5: the no-external-entity posture is documented at the XML entry modules of all three parsing crates and locked by a behavioral regression test (DOCTYPE entity stays literal, never resolved). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The ODT writer emits <style:style style:family="table"> with style:table-properties (width / alignment / background), but import only restored the table's style-name reference — the definition was written-but-not-re-read, so a round-tripped document lost its table styles' geometry. - reader: parse_style_props now collects style:table-properties via a new styles_table.rs submodule and returns a bundled ParsedStyleProps struct instead of a 5-tuple (retiring the clippy::type_complexity allow; both baselined files shrank and the ratchet was tightened). - model: OdfStyle.table_props / OdfTableProps (raw attribute strings, mirroring OdfCellProps). - mapper: a new OdfStyleFamily::Table arm maps the definition into catalog.table_styles via map_table_style_props — the exact inverse of write/table_style.rs (style:width / style:rel-width / table:align / fo:background-color). Tests: 2 mapper unit tests; table_style_name_reference_round_trips now asserts the definition (width/alignment/background) survives the full export -> import cycle, plus a percent-width round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Word semantics (ECMA-376 17.3.1.4): consecutive paragraphs with identical border settings form one bordered group — previously each member drew its own full box (doubled rules at every boundary) and w:between was imported but never rendered. New flow_para_between.rs decides group membership with a cheap child-wins probe of the five border fields (direct formatting, then the style parent chain — the para_keep_with_next pattern, no full resolve), comparing the block about to flow with its slice neighbours. The block loops stage the resulting override on FlowState; flow_paragraph applies it to the resolved top/bottom edges before layout, so the adjustment participates in the paragraph-cache key. The boundary rule (w:between, converted like any border edge) draws once as the upper member's bottom edge; a between-only group draws just the boundary rule. Scope: StyledPara runs within one block slice (top level or one nested container); keep-with-next chains and synthesized paragraphs break a group. The four ratcheted flow/para files were held at baseline via comment tightening; fidelity-status.md and the deferred-features plan updated. Tests: same_border_group_draws_between_rule_once, different_borders_do_not_group, between_only_group_draws_just_the_boundary_rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…re-colors (4c.4) ThemeVariant::Light existed as a name only — no light values, no consumer of use_theme(), no way to switch. Now: - tokens/palette.rs: ThemePalette carries one field per color token; dark() is built from the COLOR_* constants (zero drift by construction), light() is the inverted set (chrome surfaces lighten, on-chrome text darkens, accents deepen for contrast; the document page, error banner, and page-side tokens stay shared). The constants remain the dark values so all ~400 existing call sites render unchanged and migrate opportunistically. - theme.rs: AtThemeContext is now Signal-backed with palette()/toggle(); the variant->palette mapping lives on ThemeVariant as pure, tested methods. use_theme() falls back to a component-local Dark context when no root provided one (the use_breakpoint resilience pattern). - Shell chrome migrated to the palette: AtTitleBar, AtTabBar, AtDocumentTab, AtStatusBar re-color live on toggle. - AtTabBar gains a self-contained Dark/Light toggle button (rendered only when the app passes the new theme_toggle_aria_label prop - wired in all three apps with a new shell.ftl key; 44px touch target). - title_bar.rs also gains the inline-editable title (TODO(title-edit)): an optional on_rename prop swaps the center span for a draft-owning input (Enter/blur commits, Escape resets; keyed by the committed title so external renames reseed) - read-only behavior unchanged when the prop is absent. Tests: palette drift-guard, chrome-token divergence, shared-token, well-formedness; ThemeVariant palette/toggle unit tests (extracted to theme_tests.rs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…vider, platform detect, font lock (4c.3/4c.4) Built on the light-theme commit; together these close every 4c item except the two deliberately-gated ones (router->tab navigation is an architectural change; SVG icons wait on Blitz SVG support). - Template browser (TODO(browse-templates)): new AtTemplateBrowser overlay component (AtConfirmDialog mounting contract, scrollable 44px rows, theme-palette colors, i18n-agnostic props). loki-text's Browse... card is now live: a TemplateBrowserHost boundary component (ADR-0013) in home_util.rs mounts the dialog and forwards the selection to the same handler the gallery cards use. home.rs held at its 331-line baseline; 2 new home.ftl keys. - Blank-doc (TODO(tabs), separable half): the tab strip's + button opens a blank document directly (push_new_tab(new_blank_tab())) instead of navigating Home. The router->tab-driven navigation half stays deferred as an architectural change. - Ribbon divider (TODO(ribbon)): AtRibbonGroup gains show_divider (default true, so existing call sites are unchanged); AtRibbonGroups suppresses the trailing divider on the last in-strip group and for groups hosted in the vertical overflow menu. - Platform (TODO(platform)): Platform::detect()/from_os_name resolve the real host OS (std::env::consts::OS), unit-tested; apps can pass the detected variant to platform-sensitive chrome. - Fonts (TODO(font)): launch-time registration was already wired everywhere; now regression-locked by ui_font_registration.rs (all seven bundled families resolve in a Parley FontContext) and the two stale TODO notes retired. CLAUDE.md's theme section and the deferred-features plan are updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Every mounted page tile owned its own FontDataCache, and get_or_insert clones the full face bytes per entry - so a face used on M tiles was duplicated M times (a few MB to ~10MB of steady-state waste, bounded only by viewport virtualization). DocPageSource - the Arc-shared object that outlives all tiles - already held a Mutex<FontDataCache> for the standalone render path; the GPU tile path now locks that same cache instead of carrying a per-tile copy (tiles render serially on the frame loop, so the lock is uncontended; poison recovery matches the file's other locks). New BM-9 guard bench font_cache_dedup: 8 simulated tiles inserting a 1MB face allocate 8.0MB under per-tile caches vs 1.0MB under the shared cache; the assertion trips if the shared path ever re-clones per tile. page_paint_source.rs held at its 314-line baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…emory F3 / BM-8) A stashed tab session retained its Arc<PaginatedLayout> - Parley layouts + byte-index maps for every paragraph, megabytes per inactive tab, scaling with open-tab count. DocSession now keeps only the model (CRDT, Arc<Document>, cursor/undo/save state), matching the pattern the spreadsheet and presentation session maps already use. Restore recomputes the layout on the same worker thread as the open path (spawn_restore_relayout in editor_layout_task.rs): the canvas shows the fresh-open loading indicator (total_pages = 0) for the ~tens-of-ms relayout window, then publish_restore_layout lands the layout under two guards - the path must still be the restored tab (mid-relayout tab switch discards) and the generation must be untouched (an edit racing the worker relaid out already; its layout wins and the stale restore is dropped). The publish leaves state.document alone, so no extra Document clone is paid. Tests: publish-lands / yields-to-racing-edit unit tests. New BM-8 bench session_layout_residual quantifies the win: 6 stashed 120-para tabs hold 22.7MB with retained layouts vs 5.2MB model-only - about 2.9MB saved per inactive tab - with a 2x tripwire assertion against the layout creeping back into the stash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Import: docx/reader/settings.rs parses <w:mirrorMargins/> into DocxSettings; the mapper surfaces it as the new DocumentSettings.mirror_margins (serde(default), so old JSON snapshots and the Loro settings bridge round-trip it for free). Layout: LayoutOptions.mirror_margins (OR-ed in from document settings by effective_options); finish_page swaps left/right margins on even (verso) pages via paginate_blanks::mirrored_margins, so the inside margin faces the binding on both pages of a spread. LayoutPage already carries per-page margins consumed by the painter, so the swap is complete at the flow layer. Parity is section-local page numbering (matches document parity when sections start on odd pages). Export: write/settings.rs emits <w:mirrorMargins/> alongside evenAndOddHeaders; AuxParts wires the settings part whenever either flag is set. Tests: mirrored_margins_swap_on_even_pages_only (unit), mirror_margins_swaps_even_page_margins_end_to_end (flow), mirror_margins_round_trips (DOCX export->import), plus the Loro bridge preservation assert. Frozen-baseline files (flow.rs, layout lib.rs, mapper/document.rs) held via line-neutral comment trims; ceiling gate green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Carry: CharProps.language (OOXML w:lang / ODF fo:language+fo:country, already imported by both mappers) now reaches layout as the new StyleSpan.language (Arc<str>); the paragraph cache key covers it automatically via the Debug-fold invariant (regression-locked by a new para_cache test). Route: SpellState gains a per-language checker map keyed by normalized BCP-47 tag, resolved through loki_spell::locale::fallback_chain (SpellState::checker_for — en-GB finds an 'en' checker). Squiggle emission (para_underlays) segments each paragraph into contiguous same-checker ranges (merged by Arc identity so en-US|en-GB stay one segment and words tokenize intact across the boundary) and checks each with its own dictionary. A run tagged with a language no checker covers is skipped rather than checked against the wrong dictionary — foreign text stops drowning in false squiggles (LibreOffice behaviour). An empty map preserves the legacy check-everything mode exactly. Wire: SpellSnapshot carries the active dictionary's tag; loki-text registers the active checker under it in both ambient-state sites, so the skip behaviour works out of the box with the single loaded dictionary. Tests: mixed EN/FR routing + fallback-chain end-to-end, legacy-mode regression, checker_for unit tests, cache-key participation. Frozen files para.rs/resolve.rs held at baseline via comment condensing; clippy/fmt/ceiling gates green across the four touched crates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Root cause first: production paginated layout (layout_paginated_full) flows every section group through flow_section_group, which never routed through the column-balancing path — balancing was only reachable via direct flow_section calls, i.e. the tests. flow_section_group now delegates single-section groups to flow_section (extracted to the new flow_group.rs; flow.rs baseline ratcheted 1202 -> 1187), so real documents finally get the existing single-page balancing. Multi-page tail: clean-top PageStart checkpoints almost never exist for pages after the first (page breaks happen mid-flow_block via break_column, so the loop-top clean-page check rarely fires — verified empirically). The block loop now records a cheap *tail candidate* (multi-column flows only): the newest block observed to start a fresh page plus its pre-block resume snapshot, page number corrected for the flush observed during the block; a clean page top supersedes the guess. flow_balance replays the tail uncapped and only balances when the replay reproduces the natural last page (same item + glyph-run counts) — a last page starting mid-paragraph fails this and keeps fill-first — then binary-searches the cap and splices the balanced page over the natural last page. Earlier pages, checkpoints, and warnings are untouched; continuous group tails and footnote-bearing sections keep fill-first (documented). Tests: multi_page_two_column_section_balances_only_the_last_page, single_section_group_routes_through_column_balancing (the production routing regression), last_page_starting_mid_paragraph_keeps_fill_first; full loki-layout / loki-acid (page-count canaries) / loki-text / loki-renderer suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…import (5.10) Export: the document defaults were silently dropped on ODT export — the catalog carries them as synthetic __-prefixed styles (__Default / __DocDefault paragraph, __DefaultChar / __DocDefaultChar character), which the named-style writer deliberately skips, and nothing else wrote them. A re-opened exported document therefore lost its base font, size, and paragraph defaults. New odt/write/default_style.rs emits <style:default-style style:family="paragraph"> (from the catalog's is_default style) and family="text" (from default_character_style) as the first children of <office:styles>, reusing the existing property emitters. Import: the ODT mapper built __Default with is_default=true but never pointed catalog.default_paragraph_style at it (the registry's noted follow-up), so unstyled paragraphs in ODT documents did not resolve the document base font through effective_paragraph_style. Wired now — the ODF analogue of the OOXML w:default="1" wiring. meta_xml moved to the new write/meta.rs to keep write/styles.rs under the 300-line ceiling. Tested by default_styles_round_trip (export -> import: both families' properties + the catalog wiring); full loki-odf suite incl. schema validation green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Clippy -D warnings failure introduced in the previous commit — escape is only used by write/meta.rs now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
add_word was in-memory only: a word added to the personal dictionary was forgotten on app restart and silently dropped when switching the active language (activate_language parses a fresh checker with empty overrides). loki-spell: SpellChecker::personal_words() exposes the personal list (lowercased, sorted, session ignores excluded) so hosts can persist and replay it. loki-app-shell: new spell/personal_dict.rs stores the list as a JSON array at <data dir>/AppThere/Loki/personal-dictionary.json (Android- aware via the existing data_root; load-or-empty + silent-write-failure, the recent_documents pattern). SpellService::bootstrap replays the persisted words into the fresh checker, add_word saves the full list, and activate_language replays the in-memory list into the newly parsed checker — memory is the truth so an add whose save failed still survives the switch. The path is injectable (bootstrap_with_personal_dict), so tests use scratch files / None and never touch the user profile; the pre-existing add/ignore test now uses the hermetic constructor (a persisted 'zzqq' would have broken its pre-add assertion on rerun). Tests: personal_word_survives_activate_language, personal_word_survives_a_restart, 3 personal_dict unit tests, personal_words_lists_added_words_sorted_without_ignores. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Root enabler: both XML trees dropped attributes — the converter's XmlNode never captured them (so m:val characters on m:begChr/m:chr could not be read) and the layout MNode parser skipped them (so mfenced's own open/close were ignored and every fence rendered as parentheses). Both now retain (local name, unescaped value) pairs. Converter (docx/omml, new read_structs.rs / write_structs.rs; each mapping is its own inverse, locked by assert_stable round-trips): - m:d <-> <mfenced open/close/separators>; OMML defaults ( ) | are resolved on read so fence characters always travel explicitly. - m:nary <-> munderover/munder/mover/msubsup/msub/msup with the n-ary <mo> as base, honouring m:limLoc, m:subHide/m:supHide, and the integral default; the m:e operand travels as the following sibling and is re-consumed by a write-side sequence lookahead keyed on the big-operator character set. - m:m / m:mr <-> mtable / mtr / mtd. - m:acc <-> <mover accent="true"> (default U+0302 circumflex). - m:limLow / m:limUpp <-> munder / mover; a generic munderover nests them best-effort (stable from the second pass on). Typesetter (loki-layout math, new stacks.rs): mfenced honours its fence/separator attributes; munder/mover/munderover stack limits at script size centred above/below the base (accents hug with a minimal gap); mtable lays out a grid with per-column widths and per-row baselines, centred on the math axis. mod.rs held under the ceiling. Tests: 7 converter round-trips (default + custom delimiters, sum with limits + operand, integral subSup with hidden limit, 2x2 matrix, accent, limLow) and 4 typesetter structural tests; full loki-ooxml / loki-layout / loki-acid / loki-text / loki-odf suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The hardening pass was committed 2026-07-11 but the plan row was never updated; no code change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…B-9) B-8: the shared crate now owns the corpus layer, with loki-acid as its first consumer instead of the Text-coupled home: - appthere_conformance::corpus holds the promoted TC-* catalog (the four per-format tables moved wholesale; by_feature / cases_with_severity add the feature dimension), the Format/Severity vocabulary, and the new Fixture / Consumer traits — a consumer supplies document bytes + FixtureMeta and an import/export pair, and drives all three axes; the crate keeps zero Text-specific assumptions. - Golden/candidate discovery is generalised into golden::discovery (roots as parameters) — the SSIM/dE differ was promoted earlier. - loki-acid: catalog/severity become re-export shims (API unchanged), the acid Fixture enum implements the shared trait (axes derived from importer availability), AcidConsumer wraps the import dispatch — export honestly reports the corpus is import-only — and golden discovery delegates to the shared functions. Fixture::import and the consumer share one import_bytes dispatch. B-9: the on-disk corpus is formalised as fixtures/<format>/<id>.<ext> with goldens/<format>/<id>/, described by a code-as-manifest (corpus::manifest::MANIFEST) recording per fixture: the feature it exercises, which axes apply, the reference app + version behind its golden (LibreOffice 24.2, per CALIBRATION.md), and any tolerance override with justification (none needed since the kerning fix). Manifest tests assert on-disk existence and that visual entries carry a reference. Count correction found en route: the spec's '141 TC cases' headline miscounts — the per-format tables sum to 139, now asserted directly. ODP/ODG/PPTX importers stay gated on the unbuilt ACID PPTX generator (ratified §5.1 deferral). Tests: corpus/catalog/manifest units + the loki-acid trait-wiring test (import via Consumer, honest export failure, visual-only ODP axes); workspace clippy/fmt/ceiling green; broad workspace test sweep green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Compact-width (< 600 px) reading now renders the reflow view's type
12.5% larger instead of being a shrunk Expanded — the last deferred
piece of Spec 03 M4 (the bounded+centred measure landed earlier).
Implemented as a pure view transform, resolving the fidelity concern
that deferred it: the layout engine runs at width / scale and tiles
paint at zoom = scale, so the on-screen tile width is unchanged, the
reading measure narrows toward the comfortable ch-range, and the
document's own point sizes are never touched.
Single source (Spec 01 A-1): the new render_layout::{
reflow_type_scale, reflow_layout_tile_width_pt,
reflow_layout_content_width_pt} feed all three consumers — paint
(document_view now sets reflow zoom to the type scale instead of 1.0),
pointer hit-testing (reflow_hit_test_window gains a type_scale param
dividing the px->pt mapping), and keyboard navigation (keydown builds
its layout at the same divided width, so the ContinuousLayout cache
stays coherent across all paths). An unmeasured (<= 1 px) viewport
keeps scale 1.0 so the first frame does not flash scaled type.
loki-renderer deliberately does not depend on appthere-ui, so its
600 px threshold is a duplicated constant drift-locked by a loki-text
test against tokens::layout::BREAKPOINT_COMPACT_MAX_PX. The reflow
width/scale metrics moved to a new reflow_metrics.rs (re-exported from
render_layout) to hold the 300-line ceiling. The Android-CPU HTML
fallback keeps its fixed 16 px body (low-fidelity path, already
comfortable).
Tests: type-scale boundaries (599.9 vs 600), CSS-tile-width invariance
across the breakpoint, narrowed Compact measure, scaled hit-test
resolving to the same paragraph, and the threshold drift lock; full
loki-renderer + loki-text suites, workspace clippy, ceiling gate green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The tab strip rendered at 36 px at every breakpoint — below the WCAG 2.5.8 44 px minimum the strip's own doc comment flagged. It is now breakpoint-aware: the touch-first Compact class gets TOUCH_MIN-tall tabs and collapse toggle via the new pure tab_strip_height helper; pointer-first classes keep the 36 px desktop density (the shell tab-bar convention). Resilient without a responsive context (reads Expanded), per the established use_breakpoint contract. R-13e (condensed select sizing) was verified already built by the M3 collapse cascade: AtRibbonSelect shrinks via the ambient GroupCollapse signal. RB-11 (bottom-ribbon-for-touch) stays deliberately unbuilt — the audit marks it optional and the collapse cascade covers Compact. 4a.5 is complete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
DOCX: w:tcBorders was parsed on import all along but never written — a bordered cell exported borderless. write_table_cell now emits the four edges (style -> w:val incl. the nil explicit-none, width -> w:sz in eighth-points clamped to the schema's 2..=96, colour -> hex/auto) and the tcPr children are reordered to the CT_TcPr schema sequence (tcBorders, shd, tcMar, vAlign — shading was previously emitted after vAlign, out of order). ODT: the per-cell automatic table-cell style carried only the resolved background; it now also bakes fo:border-* (reusing the paragraph writer's ODF shorthand formatter) and fo:padding-* from the cell's direct props. Import already read both on each side, so this closes the round-trip. Tests: cell_borders_and_padding_round_trip (DOCX) and cell_borders_and_padding_round_trip_via_cell_style (ODT), each asserting style/width/colour and that a plain neighbour stays clean; full loki-ooxml + loki-odf suites (incl. schema validation over the reordered tcPr) green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
ODF table default: style:default-style family="table" now maps to a synthetic __DefaultTable catalog entry with catalog.default_table_style pointing at it — the table family's Default source (ADR-0012 Decision 1), closing the gap noted when the table resolver landed (OdfDefaultStyle previously discarded the parsed table properties). w:cnfStyle (ECMA-376 17.4.7): Word stamps each cell with a 12-flag mask naming exactly which table-style regions apply. New TableCnf codec (doc-model style/table_cnf.rs) decodes/encodes the mask; it rides the cell attr under "cnf" (the tbllook opaque-code idiom, so it round-trips through the Loro CRDT for free). The DOCX reader parses it, the mapper stamps the cell (validated masks only), and the writer re-emits it as the first CT_TcPr child. Resolution: resolve_cell_shading_cnf walks the same precedence array with membership taken from the mask instead of positional derivation — authoritative when present (survives row reordering and unusual band sizes), with positional fallback for absent/malformed masks — wired into both the layout paint (cell_style_shading_cnf) and the ODT export's baked per-cell shading, so the two stay in agreement. Tests: mask codec round-trip + malformed rejection, cnf-driven region resolution precedence, mask-beats-position (a row-1 cell stamped firstRow takes header shading; malformed falls back), DOCX cnf round-trip, ODF table-default mapper test. Four-crate suites, clippy, fmt, ceiling gates green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Model: TableConditionalFormat gains char_props (w:tblStylePr/w:rPr — the bold header row of every built-in banded style; serde(default) for pre-4a.3 catalogs). New resolvers resolve_cell_char_props(_cnf) merge applicable regions low-to-high precedence per property (wholeTable under bands under firstRow), with membership from the position+tblLook derivation or an explicit w:cnfStyle mask. OOXML: the styles reader routes a region's rPr to the region — it previously CLOBBERED the style-level rpr (latent bug, masked only because table-style rpr went unused); the mapper keeps regions with shading or char props (unshaded rPr-only regions were dropped); the writer emits the region rPr back (CT_TblStylePr order), reusing the symmetric run-props writer. Layout: a per-table character grid (new flow_table_chars.rs; None for plain tables, so they pay nothing) feeds BOTH the row-height measurement and content-flow passes — bold/large header text is wider and taller, so the two must agree. FlowState.cell_char_defaults carries the current cell's defaults into resolve::flatten_paragraph_with_base, which slots the region under the style chain per Word precedence (docDefaults < region < named styles < direct; an unstyled paragraph's chain IS docDefaults, so the region merges over it — the dominant table-cell case). flow_table was extracted to flow_table_main.rs and the stale Session-4 audit narrative left resolve.rs; flow.rs ratchets 1187 -> 1101, resolve.rs 858 -> 857. Tests: precedence merge (firstRow size beats wholeTable, italic falls through), end-to-end flow (header glyphs at the region's 20pt, body at 12pt), DOCX region-rPr round-trip; five-crate suites + clippy + fmt + ceiling gates green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Doc-only: the previous commit's plan update failed to apply (stale match); no code change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
….1, 328 → 198) Moves the run-level intermediate-model types out of paragraph.rs into a sibling submodule, driving paragraph.rs under the 300-line ceiling (it leaves the baseline): - paragraph_run.rs (run submodule): DocxRun, DocxRunChild, DocxRPr, DocxRFonts, DocxHyperlink, DocxDrawing (ECMA-376 §17.3.2). The submodule imports DocxMarkRevision via its absolute path; all six types are re-exported from paragraph.rs so every crate::docx::model::paragraph::Docx... path is unchanged. The paragraph- level types (DocxParagraph, DocxPPr, DocxInd, borders, tabs, etc.) stay. Pure code motion. loki-ooxml suite green (262 lib tests + integration binaries incl. DOCX round-trips), clippy -D warnings clean; file-ceiling baseline shrinks 23 -> 22 (paragraph.rs 328 -> 198, paragraph_run.rs 143 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…h (7.1, 331 → 210)
Moves the mutation -> relayout -> publish path out of state.rs into a
sibling submodule, driving state.rs under the 300-line ceiling (it leaves
the baseline):
- state_apply.rs (apply submodule): apply_mutation_and_relayout
(re-derives the Document from Loro incrementally, relays out reusing
unchanged pages, publishes state + bumps the generation) and its
throttled log_memory_counters instrumentation.
The submodule reaches back for super::DocumentState and imports
crate::editing::relayout::{page_metrics, relayout_paginated} directly.
apply_mutation_and_relayout is re-exported from state.rs so
crate::editing::state::apply_mutation_and_relayout is unchanged for its
many callers.
Pure code motion. loki-text suite green (207 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 22 -> 21
(state.rs 331 -> 210, state_apply.rs 140 under the ceiling).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ta (7.1, 331 → 284) Moves the Home screen's static data out of home.rs into a sibling submodule, driving home.rs under the 300-line ceiling (it leaves the baseline): - home_templates.rs (templates submodule): MIME_TYPES (the file-picker accept list) and make_templates (the localized builtin-template gallery cards). The submodule imports only appthere_ui::BuiltinTemplate + loki_i18n::fl; both items are re-imported into home.rs. The Home component and its file- picker / routing wiring stay. Pure code motion. loki-text suite green (207 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 21 -> 20 (home.rs 331 -> 284, home_templates.rs 60 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
… (7.1, 306 → 226) Moves the two-pass w:vMerge span resolver out of table.rs into a sibling submodule, driving table.rs under the 300-line ceiling (it leaves the baseline): - table_vmerge.rs (vmerge submodule): compute_v_merge_spans — assigns row_span to restart cells and marks continuation cells for omission (OOXML §17.4.84). Re-imported into table.rs; DocxVMerge threaded into table_tests.rs (which exercises compute_v_merge_spans directly). Four now-unused imports dropped. Pure code motion. loki-ooxml suite green (262 lib tests + integration binaries incl. DOCX round-trips), clippy -D warnings clean; file-ceiling baseline shrinks 20 -> 19 (table.rs 306 -> 226, table_vmerge.rs 91 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…7.1, 312 → 138) Moves the ODF style property-value model types out of styles.rs into a sibling submodule, driving styles.rs under the 300-line ceiling (it leaves the baseline): - styles_props.rs (props submodule): OdfParaProps, OdfDropCap, OdfTabStop, OdfTextProps, OdfCellProps (raw-attribute-string value bags for lossless round-tripping). The submodule is fully self-contained (no imports); all five types are re-exported from styles.rs so every crate::odt::model::styles::Odf... path is unchanged. The aggregate / style / family types stay. Pure code motion. loki-odf suite green (245 lib tests + integration binaries incl. ODT round-trips), clippy -D warnings clean; file-ceiling baseline shrinks 19 -> 18 (styles.rs 312 -> 138, styles_props.rs 186 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ers (7.1, 325 → 296) Moves the reuse-boundary helpers out of incremental.rs into a sibling submodule, driving incremental.rs under the 300-line ceiling (it leaves the baseline): - incremental_diff.rs (diff submodule): common_prefix_len / common_suffix_len / blocks_equal_from (block-list prefix/suffix diff that locates the changed range) and section_page_start (checkpoint -> starting-page lookup). The submodule imports Block + super::PageStart; all four are re-imported into incremental.rs. The now-unused Block import dropped from incremental.rs. Pure code motion. loki-layout suite green (278 lib tests + integration binaries incl. the incremental == full property test), clippy -D warnings clean; file-ceiling baseline shrinks 18 -> 17 (incremental.rs 325 -> 296, incremental_diff.rs 42 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ts (7.1, 324 → 82) Moves the top-level layout entry points out of the crate root into a module, driving lib.rs under the 300-line ceiling (it leaves the baseline): - layout_entry.rs: layout_document (paginated/continuous dispatch), layout_paginated_full (paginated + the incremental-reuse checkpoints), and effective_options (folds document settings into the caller's LayoutOptions). The module uses explicit crate:: imports; layout_document and layout_paginated_full are re-exported from the crate root so the public API is unchanged. lib.rs is now just module wiring + re-exports + two pub consts. Pure code motion. loki-layout suite green (278 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 17 -> 16 (lib.rs 324 -> 82, layout_entry.rs 262 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…a helpers (7.1, 316 → 237) Moves the style-picker data helpers out of editor_style.rs into a sibling submodule, driving editor_style.rs under the 300-line ceiling (it leaves the baseline): - editor_style_data.rs (data submodule): collect_style_names (enumerates the document's (display_name, style_key) pairs: built-in headings + catalog styles) and style_preview_font (per-chip preview font). collect_style_names is re-exported (pub); style_preview_font re-imported. The style_picker_panel render fn and PICKER_HEIGHT_PX const stay; the now-unused StyleId import dropped. Pure code motion. loki-text suite green (207 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 16 -> 15 (editor_style.rs 316 -> 237, editor_style_data.rs 98 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…→ 384) Moves the positioned-item painting cluster out of scene.rs into a sibling submodule: - scene_items.rs (items submodule): paint_items (walks a [PositionedItem] slice, dispatching each variant to the per-kind painters crate::glyph/rect/image/decor and recursing into clipped/rotated groups), paint_link_hint (hyperlink underlay), and translate_item (leaf shift). The cluster references no other scene.rs item (fully self-contained). paint_items is re-exported pub(crate) (also used by band.rs); the extracted test module's #[cfg(test)] gate and translate_item import were re-threaded. Pure code motion. loki-vello suite green (22 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline ratcheted (scene.rs 613 -> 384, scene_items.rs 244 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…ter (7.1, 384 → 263)
Moves paint_single_page out of scene.rs into a sibling submodule, driving
scene.rs under the 300-line ceiling (it leaves the baseline):
- scene_single_page.rs (single_page submodule): paint_single_page — draws
one paginated page (L-shaped drop shadow, white background,
content/header/footer/comment items, rotation-aware cursor + selection
overlay).
It reaches back for super::{CursorPaint, the page-chrome consts,
page_chrome_size, paint_items}; re-exported pub from scene.rs (called by
paint_layout + externally).
Pure code motion. loki-vello suite green (22 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 15 -> 14
(scene.rs 384 -> 263, scene_single_page.rs 140 under the ceiling).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…→ 277) The file was one 183-line LokiPageSource::render method over a 314-line file. Two self-contained render phases move into a sibling submodule: - page_paint_render.rs (render submodule): allocate_page_texture (GPU texture + view allocation) and page_cursor_paint (page-relative cursor paint data, layout guard scoped internally). Both take only render-loop-passed data — no LokiPageSource state beyond the source handle. The now-unused anyrender_vello::wgpu import list moved with them. Pure code motion. loki-renderer suite green (18 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 14 -> 13 (page_paint_source.rs 314 -> 277, page_paint_render.rs 81 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts the mutually-recursive body-child dispatcher into a sibling submodule:
- document_body.rs (body submodule): read_section (text:section) and
read_body_children (the shared body-children loop dispatching
text:p/h/list/table/table-of-content/section/tracked-changes/indexes).
It reaches the parent's leaf readers via super::{read_list, read_paragraph,
read_table, read_toc, skip_element} and calls read_tracked_changes by its
full crate path. read_body_children is re-exported into the parent (used by
read_document); the now-unused OdfSection import was dropped from document.rs.
Pure code motion. loki-odf suite green (245 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 13 -> 12
(document.rs 367 -> 276, document_body.rs 111 under the ceiling).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts the shared parse-and-map pipeline into a sibling submodule: - import_package.rs (package submodule): parse_and_map_package (~160-line workhorse — opens the OPC package, locates officeDocument, parses body/styles/numbering/notes/settings/comments, collects hyperlink/image/header/footer related parts, calls map_document) plus its four OPC part/rels resolution helpers (map_xml_err, resolve_part_name, rels_by_type, resolve_optional_part + the OpcResult alias) and the resolve_part_name tests. parse_and_map_package is re-exported pub(crate) from import.rs (used by DocxImporter::run and crate::docx::mapper). The two helpers still needed by the sibling import_pic_bullets were promoted pub(super) and its super:: calls repointed at super::package::. The twelve now-unused imports were dropped from import.rs. Pure code motion. loki-ooxml suite green (262 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 12 -> 11 (import.rs 387 -> 129, import_package.rs 279 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts the ZIP-entry reading helper cluster into a sibling submodule: - package_read.rs (read submodule): validate_mimetype, read_entry (capped read + UTF-16->UTF-8 transcode), collect_images (Pictures/), collect_objects (embedded content.xml sub-docs), plus the internal transcode_utf16_to_utf8 / infer_media_type and the four infer_media_type tests. The four impl-facing entry points are promoted pub(super) and re-imported into package.rs. The XML local_name_bytes helper stays with the struct/impl. Now-unused imports (CompressionMethod, read_entry_capped, five mimetype constants) were dropped from package.rs (the MIME_ODT doc-link repointed at its full path); the parent test file gained explicit ENTRY_MIMETYPE/MIME_ODT imports. Pure code motion. loki-odf suite green (245 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 11 -> 10 (package.rs 410 -> 223, package_read.rs 233 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…→ 249)
Extracts two cohesive element-parsing clusters into sibling submodules:
- document_ppr.rs (ppr submodule): the w:pPr cluster — parse_ppr_element +
apply_ppr_attr + parse_pbdr + parse_tabs. parse_ppr_element is re-exported
pub(crate) (external callers in styles.rs / numbering.rs).
- document_drawing.rs (drawing submodule): the w:drawing cluster —
parse_drawing + parse_wrap_text. parse_drawing is re-exported pub(crate)
(called by the run submodule).
Both reach shared readers via crate::docx::reader::{util,sectpr} and
super::parse_rpr_element. Fourteen now-unused imports (nine Docx* paragraph
-prop types, parse_emu/toggle_prop, FloatWrap/TextWrap/WrapSide) were dropped
from document.rs.
Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 10 -> 9
(document.rs 540 -> 249, document_ppr.rs 233 / document_drawing.rs 93 under
the ceiling).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
… 229)
Extracts two cohesive writer clusters into sibling submodules:
- document_para.rs (para submodule): the paragraph writers — write_para,
write_styled_para, and the w:pPr child emitter write_para_props_inline.
write_para / write_styled_para are re-exported for the write_block
dispatcher and the blocks submodule.
- document_blocks.rs (blocks submodule): the non-paragraph block writers —
write_code_block, write_horizontal_rule, write_line_block, write_list_item.
All are re-exported; the recursive write_list_item reaches back for
super::{write_block, write_para}.
RunProps (referenced externally by fields.rs / document_inlines.rs) stays in
document.rs; child modules construct it via field-privacy-through-descendants.
Shared writers are reached via crate::docx::write::{xml,run_props,revision}
and super::inlines; the super::revision::write_mark_del call was repointed at
its full crate path; six now-unused imports were dropped.
Pure code motion. loki-ooxml suite green (262 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 9 -> 8
(document.rs 540 -> 229, document_para.rs 227 / document_blocks.rs 125 under
the ceiling).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts three cohesive units out of the styles reader: - styles_list_style.rs (list_style submodule): parse_list_style — the text:list-style reader dispatching bullet/number/image/none levels via the existing list submodule. Re-exported for read_stylesheet. - styles_auto.rs (auto submodule): read_auto_styles (the content.xml office:automatic-styles fast reader) + parse_style_family. read_auto_styles re-exported pub(crate) for odt/import.rs; parse_style_family re-exported for read_stylesheet. - parse_text_props_attrs (the style:text-properties attribute reader) moved into the existing props submodule (cohesive with parse_style_props) and re-exported so the list/props submodules' super::parse_text_props_attrs paths keep working. Six now-unused type imports were trimmed from styles.rs; the extracted test file gained explicit OdfStyleFamily/OdfListLevelKind imports. Pure code motion. loki-odf suite green (245 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 8 -> 7 (styles.rs 599 -> 295; styles_list_style.rs 182 / styles_auto.rs 128 / styles_props.rs 217 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts two cohesive placement clusters into sibling submodules of para_impl: - flow_para_place.rs (place submodule): place_paragraph_layout — continuous vs paginated placement, keep-together, and line-boundary splitting. Re-exported for flow_paragraph and the chain cluster. - flow_para_chain.rs (chain submodule): the keep-with-next chain cluster — flow_keep_with_next_chain + build_chain_layouts + place_chain_blocks + place_chain_too_tall + the CHAIN_LIMIT const. flow_keep_with_next_chain is pub(crate), re-exported pub(super) from para_impl for flow.rs. Both child modules reach the flow-state helpers (break_column, finish_page, push_editing_para, split_and_place_loop, FlowState, LayoutWarning) through para_impl's existing imports via super::, and the block synthesizers via super::super:: (the flow module). place_paragraph_layout is shared between flow_paragraph and the chain via the place re-export. Two now-unused imports (Arc, Block) were dropped from flow_para.rs. Pure code motion. loki-layout suite green (278 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 7 -> 6 (flow_para.rs 522 -> 236; flow_para_place.rs 96 / flow_para_chain.rs 225 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Two-file split of the inline-flattening machinery (the walk_inlines monolith):
- resolve_walk.rs (walk submodule): walk_inlines — the recursive inline-tree
walker (~277 lines), pub(super).
- resolve_inlines.rs (inlines submodule): the flattening leaves —
flatten_paragraph_with_base (entry, re-exported pub for flow_para /
flow_para_chain), the text emitters push_text/push_small_caps/apply_link,
field_display_text, superscript_mark, and the Inline::Image arm extracted as
collect_inline_image.
The two submodules are mutually recursive: inlines::flatten_paragraph_with_base
and collect_inline_image call super::walk::walk_inlines; walk_inlines calls
super::inlines::{push_text, field_display_text, superscript_mark,
collect_inline_image}. Extracting the Image arm kept the walk file under the
ceiling. resolve.rs keeps the pure resolvers (resolve_color/pts_to_f32/
emu_to_pt, CollectedImage/CollectedNote, resolve_para_props/resolve_char_props,
the flatten_paragraph wrapper, convert_border). NodeAttr was imported from its
real content::attr path; eight now-unused imports were trimmed.
Pure code motion. loki-layout suite green (278 lib tests + integration
binaries), clippy -D warnings clean; file-ceiling baseline shrinks 6 -> 5
(resolve.rs 665 -> 194; resolve_walk.rs 266 / resolve_inlines.rs 266 under
the ceiling).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Extracts three cohesive clusters into sibling submodules; flow.rs keeps only the public types (FlowOutput/LayoutWarning), the FlowState struct/impl, and the module wiring: - flow_run.rs (run submodule): new_flow_state + run_paginated_loop — the flow-state constructor and top-level paginated block loop. - flow_entry.rs (entry submodule): flow_section + flow_section_resume + begin_continuous_section — the public section entry points. - flow_dispatch.rs (dispatch submodule): flow_block + flow_blocks + finish_page — block dispatch and page finalization. Every function reaches the other clusters and the fourteen existing sibling submodules through flow.rs's imports via super:: (children see the parent's private use-imports). flow_block/finish_page were promoted pub(crate) in dispatch so flow.rs can re-export them pub(super) for the external submodule callers (flow_para/flow_tail/flow_balance/flow_group/flow_table_geom); new_flow_state/run_paginated_loop/begin_continuous_section stay reachable as super::* for flow_balance/flow_group. Seven now-unused imports were trimmed. Pure code motion. loki-layout suite green (278 lib tests + integration binaries), clippy -D warnings clean; file-ceiling baseline shrinks 5 -> 4 (flow.rs 721 -> 277; flow_run.rs 171 / flow_entry.rs 175 / flow_dispatch.rs 167 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…rtial, 834 → 770) Moves the tab-stop expansion probe pass (gap #7 — lays the paragraph out with zero-width inline boxes at each \t / decimal separator to measure x-positions, then resolves each into a TabPlan) out of the 580-line layout_paragraph_uncached into para_tabs.rs::measure_tab_plans — the module that already owns compute_tab_plans / emit_tab_leader / TabPlan, colocating the tab logic. Reaches back for super::{push_para_styles, push_math_inline_boxes, DEC_ID_BASE, END_ID}. Partial: para.rs 834 -> 770 (baseline cap ratcheted down but NOT cleared). Unlike the files cleared this pass, para.rs is dominated by one cohesive 580-line algorithm whose phases share heavily-mutated state, and the natural home for the remaining big phase (para_emit.rs) is already at the ceiling; clearing it under 300 would require gutting the core paint path. See the plan doc for the honest status and recommendation to keep para.rs (+ the three Dioxus components) as deliberate baseline entries. Pure code motion. loki-layout suite green (278 lib tests + integration binaries), clippy -D warnings clean; baseline stays at 4 (para.rs cap 834 -> 770; para_tabs.rs 280 under the ceiling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
…low]` Mirrors the file-ceiling ratchet for the audit's Q-3/Q-4 debt: - scripts/check-suppressions.py counts `let _ = …` (discarded/ swallowed Results) and `#[allow(…)]`/`#![allow(…)]` (lint suppressions) per production .rs file (test files excluded, same scope as the file-ceiling gate). - scripts/suppressions-baseline.txt freezes the current population per file: 419 `let _ =`, 170 `#[allow]` across 134 files. - Ratchet rules match the file ceiling: a non-baselined file must be 0/0; a baselined file's two counts may only shrink; a file dropping to 0/0 must leave the baseline; a stale entry (missing/renamed file) fails. So adding a genuinely-needed suppression becomes a deliberate, reviewable `--update` rather than silent accretion. - Wired into .github/workflows/rust.yml right after the file-ceiling gate. First opportunistic reduction: a strip-and-rebuild probe showed most OOXML `#[allow(dead_code)]` are stale (removing 17 produced only 11 real dead-item warnings). docx/model/styles.rs's five were all unnecessary — removed; clippy -D warnings stays clean, `#[allow]` total 175 -> 170. The genuinely-dead items keep their now-baselined allows (deleting intentional model/round-trip surface is the big-bang the audit warns against; left for per-file follow-up). loki-ooxml suite green (262 lib tests + integration binaries), cargo check --workspace clean; both ratchet gates pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The `cargo udeps` piece of the Spec 01 residuals, done as a stable-toolchain stand-in (cargo-udeps isn't installable here): a per-crate heuristic scan surfaced 9 candidates, each authoritatively verified by removal + cargo check. Three genuinely-unused deps removed (each confirmed to compile without it, the optional one under its own feature): - loki-pdf: loki-primitives (unreferenced). - loki-server-api: tokio — the [dependencies] `sync`-feature entry; the [dev-dependencies] tokio used by tests stays. - loki-sheet-model: serde_json (+ its `dep:serde_json` entry in the `serde` feature) — the feature's cfg_attr derives need only `serde`, not serde_json. The other six candidates were false positives the compile-check correctly rejected and kept: appthere-color / loki-primitives (used via non-`::` paths), async-trait (attribute-macro use), and fontique x3 (a deliberate feature-enabler forcing fontconfig-dlopen across the workspace, documented in each app manifest). The pedantic rollout (2/43 crates today), the no_hardcoded_layout_dims dylint (needs the nightly dylint driver; the viewport-dims Python gate stands in), and Android-target CI (needs the NDK — `cargo check` for the target fails on a -sys cc build here) remain toolchain-gated; see the plan doc. Cargo.lock collapsed by one entry; cargo check --workspace clean, loki-pdf + loki-sheet-model suites green (51 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
Adds the audit T-3 "still open" per-case P0 round-trip assertions. These use a build -> export -> re-import -> assert-present shape rather than the divergence helper (conformance_round_trip.rs), which compares two consecutive cycles and is blind to loss on the FIRST export (seed->a drops a property, a->b keeps it dropped, a==b passes falsely). New tests/conformance_p0_round_trip.rs (DOCX) + conformance_xlsx_p0_round_trip.rs (XLSX), 5 cases — and, per the fix-the-cause principle, the three real exporter/importer gaps they exposed are fixed: - TC-DOCX-021 (bookmark + REF/PAGEREF cross-reference): already round-tripped; pure test addition. - TC-DOCX-023 (floating-image wrap modes): the DOCX writer emitted only wp:inline and ignored the image's FloatWrap, so every floating image exported as inline and lost its wrap. Added write_anchor_drawing emitting w:drawing/wp:anchor + the wrap element (wrapSquare/Tight/Through/ TopAndBottom/None + wrapText side + behindDoc), sharing the pic:pic body with the inline path via a new write_pic_graphic. Anchor position is unmodelled (reader recovers only wrap + behindDoc) -> zero offset. - TC-DOCX-029 (RTL/bidi): w:bidi was read on import and mapped to ParaProps.bidi but never written on export; added the w:bidi toggle emit to write_para_props_inline. (ODT export already wrote style:writing-mode.) - TC-XLSX-009/011 (dynamic-array spill + XLOOKUP/modern functions): the <f> text handler OVERWROTE current_formula per text event instead of appending, so any formula containing an XML-escaped char (>/</&/" — ubiquitous in comparison formulas like FILTER(A1:A10,B1:B10>0)) kept only the fragment after the last entity (e.g. "0)"). Changed to get_or_insert_with(String::new).push_str, matching the value handler. ODT export coverage (T-2) was already satisfied (odt/write/ ships with odt_export_round_trip.rs + metadata_round_trip.rs). Hard PPTX cases (T-5) stay deferred — they need a PowerPoint-authored deck that can't be produced here. Also fixed unused-import warnings the earlier 7.1 xlsx-split left in xlsx/import.rs + xlsx/export.rs — feature-gated, so invisible without --all-features (which CI runs), and would have failed clippy -D warnings. Suppression baseline (7.3): document_drawing.rs `let _ =` 35->66 (new writer follows the file's `let _ = write_*` house idiom) while its #[allow] dropped 2->1 (renamed cx_s/cy_s, removing the similar_names allows); document_para.rs `let _ =` 22->24 (two bidi writes). Regenerated deliberately. Docs: fidelity-status.md bidi/RTL (export No->Yes) and Floating Images (export No->Partial) rows updated; deferred-features-plan 7.5 row marked done. loki-ooxml --all-features suite green (205 lib + integration incl. the 5 new cases), workspace clippy --all-features -D warnings clean, fmt clean, file-ceiling + suppression gates green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The three app shells (loki-text/loki-spreadsheet/loki-presentation) carried
byte-identical (bar doc-comment wording) tab-management logic in
routes/shell.rs::close_tab and routes/home_util.rs::{push_or_switch_tab,
close_tab_for_path}. The bug-prone active-tab / navigation index arithmetic is
now single-sourced in loki_app_shell::tabs as pure, dependency-free functions:
- open_or_switch(&mut Vec<OpenTab>, path, title) -> usize
- close_by_path(&mut Vec<OpenTab>, active, path) -> usize
- resolve_tab_close(idx, current_active, len_before) -> Option<TabCloseOutcome>
(with TabCloseNav::{Stay, Home, Editor(idx)})
Each app keeps a thin Signal wrapper that reads/writes its own signals, drops
its app-specific DocSessions entry, and applies the router nav — so no Dioxus
dependency is added to loki-app-shell and it stays headlessly buildable. The
logic is now covered by 13 unit tests (tabs_tests.rs) instead of being
copy-pasted 3x untested.
Deliberately not extracted (documented residuals):
- The Shell component + Route enum: a Dioxus #[component] can't be generic
over a per-app Routable, and each app's Route/DocSessions/ribbon differ, so
the component shell and route construction stay per-app.
- display_title_from_path + helpers (~90 lines x3): moving it needs
loki-app-shell to depend on loki-file-access, which target-gates rfd ->
wayland-sys on desktop Linux (not a Cargo feature), making the shared-logic
crate un-buildable headlessly. Better closed upstream by gating rfd behind a
loki-file-access feature; left as a documented residual.
Verification: installed libwayland-dev/libxkbcommon-dev to unlock the full GUI
build, then compiled + clippy-cleaned all three apps and loki-app-shell (exit
0; only the pre-existing vendored blitz-dom dependency warning); 13 new unit
tests pass; fmt clean; file-ceiling / suppression / license gates green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
The 6.5 mandate was "re-measure first, then fix if confirmed." Measured all three; the gate did its job — none justify a change now. P-3 (glyph-run span scans) — measured, refuted. New permanent bench (loki-bench/benches/portable_span_density.rs) lays out a fixed 256-word paragraph split into N distinct style spans (cache cleared each rep → cold emit path) and microbenches the isolated find-per-run scan vs a byte→span index map. End-to-end layout is strongly sub-linear in span count (4→256 spans = x2.4, not the ~x4000 a dominant O(N^2) implies; Parley shaping dominates). The isolated scan is ~0.7% of layout at N=64 and ~6% at a pathological N=256, <0.1% at realistic <20 spans/para. Decisively, the proposed byte→span-index map is SLOWER for small N (map build out-costs a short scan), so it would regress the common case. Also 3 of the 4 named scanners are already gone (moved to selection-geometry underlay passes) and the per-call range.clone() is gone. No fix; bench kept as a regression guard. P-5 (coarse render invalidation) — confirmed coarse, fix device-gated. The flagged page_cache.rs::mark_all_dirty no longer exists; invalidation is now loki-renderer's generation-keyed DocPageSource (a mutation bumps the generation → all visible tiles re-render). Bounded by viewport virtualization; the real payoff is GPU frame-time (device axis, not headless-measurable) and the fix is a substantial per-page render-dirty refactor. Deferred to the device-frame-time bench rather than speculatively refactored. P-6 (cold-path clones) — confirmed negligible. snapshot_checkpoint clones list_counters (empty for list-free docs) per page boundary at layout time, dwarfed by Parley allocations. Opportunistic-only, per the audit's P3 rating. Docs: plan 6.5 row + audit P-3/P-5/P-6 entries updated with the measurements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is a large refactoring pass that splits 43+ production files exceeding the 300-line ceiling into focused, single-responsibility modules. The changes enforce the file-ceiling convention mechanically (via CI) and reduce the worst offenders from 600+ lines to ≤300 lines each.
Key Changes
Layout engine (
loki-layout)flow.rs: Split intoflow_entry.rs,flow_run.rs,flow_dispatch.rs,flow_tail.rs,flow_group.rs,flow_headers.rsflow_para.rs: Split intoflow_para_chain.rs,flow_split.rs,flow_para_place.rs,flow_para_between.rspara.rs: Split intopara_build.rs,para_layout_types.rs,para_types.rs,para_emit.rs(existing)resolve.rs: Split intoresolve_char_span.rs,resolve_inlines.rs,resolve_walk.rslib.rs: Addedlayout_entrymodule for top-level entry pointsODF reader/writer (
loki-odf)odt/reader/styles.rs: Split intostyles_props.rs,styles_list_style.rs,styles_auto.rs,styles_table.rsodt/reader/document.rs: Split intodocument_body.rs,document_list.rs,document_toc.rsodt/model/styles.rs: Split intostyles_props.rs(model types)odt/write/mod.rs: Addeddefault_style.rs,meta.rsmodulespackage.rs: Split intopackage_read.rsfor ZIP-entry helpersods/import.rs: Split intoods/import_helpers.rsods/export.rs: Split intoods/export_content.rsOOXML reader/writer (
loki-ooxml)docx/import.rs: Split intodocx/import_package.rsfor parse-and-map pipelinedocx/mapper/document.rs: Split intodocx/mapper/document_page.rsdocx/mapper/inline.rs: Refactored (imports reduced)docx/model/paragraph.rs: Split intodocx/model/paragraph_run.rsdocx/omml/read.rs: Split intodocx/omml/read_structs.rsdocx/omml/write.rs: Split intodocx/omml/write_structs.rsdocx/reader/document.rs: Split intodocument_drawing.rs,document_ppr.rs,document_run.rsdocx/write/document.rs: Split intodocument_blocks.rs,document_drawing.rs,document_inlines.rs,document_para.rsxlsx/export.rs: Split intoxlsx/export_xml.rsxlsx/import.rs: Split intoxlsx/import_styles.rs,xlsx/import_worksheet.rsModel and conformance
loki-doc-model: Addedstyle/table_cnf.rs(explicit conditional formatting)appthere-conformance: Promoted corpus vocabulary fromloki-acidintocorpus/mod.rs,corpus/manifest.rs,corpus/catalog/mod.rsloki-acid: Refactoredcatalog/mod.rsandseverity.rsto use new conformance corpusUI and shell
appthere-ui: Addedtokens/palette.rs(theme-variant color palette),components/template_browser.rsloki-app-shell: Addedspell/personal_dict.rs,tabs_tests.rsloki-text: Split editor state intohttps://claude.ai/code/session_01RTUeFoETarsoQium1NzuPo